<template>
	<table @click="handleMonthTableClick" @mousemove="handleMouseMove" class="el-month-table">
		<tbody>
			<tr v-for="(row, key) in rows" :key="key">
				<td :class="getCellStyle(cell)" v-for="(cell, key) in row" :key="key">
					<div>
						<a class="cell">{{ getCellText(cell) }}</a>
					</div>
				</td>
			</tr>
		</tbody>
	</table>
</template>
<script lang="ts">
export default async function () {
	const [{ isDate, range, getDayCountOfMonth, nextDate }] = await _.$importVue([
		"/common/ui-x/components/form/xDatePicker/dateUtils.vue"
	]);
	const { hasClass } = _xUtils;

	const datesInMonth = (year, month) => {
		const numOfDays = getDayCountOfMonth(year, month);
		const firstDay = new Date(year, month, 1);
		return range(numOfDays).map(n => nextDate(firstDay, n));
	};

	const clearDate = date => {
		return new Date(date.getFullYear(), date.getMonth());
	};

	const getMonthTimestamp = function (time) {
		if (typeof time === "number" || typeof time === "string") {
			return clearDate(new Date(time)).getTime();
		} else if (time instanceof Date) {
			return clearDate(time).getTime();
		} else {
			return NaN;
		}
	};

	// remove the first element that satisfies `pred` from arr
	// return a new array if modification occurs
	// return the original array otherwise
	const removeFromArray = function (arr, pred) {
		const idx = typeof pred === "function" ? _.findIndex(arr, pred) : arr.indexOf(pred);
		return idx >= 0 ? [...arr.slice(0, idx), ...arr.slice(idx + 1)] : arr;
	};

	return defineComponent({
		props: {
			disabledDate: {},
			value: {},
			selectionMode: {
				default: "month"
			},
			minDate: {},

			maxDate: {},
			defaultValue: {
				validator(val) {
					// null or valid Date Object
					return val === null || isDate(val) || (Array.isArray(val) && val.every(isDate));
				}
			},
			date: {},
			rangeState: {
				default() {
					return {
						endDate: null,
						selecting: false
					};
				}
			}
		},

		watch: {
			"rangeState.endDate"(newVal) {
				this.markRange(this.minDate, newVal);
			},

			minDate(newVal, oldVal) {
				if (getMonthTimestamp(newVal) !== getMonthTimestamp(oldVal)) {
					this.markRange(this.minDate, this.maxDate);
				}
			},

			maxDate(newVal, oldVal) {
				if (getMonthTimestamp(newVal) !== getMonthTimestamp(oldVal)) {
					this.markRange(this.minDate, this.maxDate);
				}
			}
		},

		data() {
			return {
				tableRows: [[], [], []],
				lastRow: null,
				lastColumn: null
			};
		},

		methods: {
			getCellText(cell) {
				const months = [
					"jan",
					"feb",
					"mar",
					"apr",
					"may",
					"jun",
					"jul",
					"aug",
					"sep",
					"oct",
					"nov",
					"dec"
				];
				const propString = `el.datepicker.months.${months[cell.text]}`;
				const cellString = i18n(propString);
				return cellString;
			},
			cellMatchesDate(cell, date) {
				const value = new Date(date);
				return (
					this.date.getFullYear() === value.getFullYear() &&
					Number(cell.text) === value.getMonth()
				);
			},
			getCellStyle(cell) {
				const style = {};
				const year = this.date.getFullYear();
				const today = new Date();
				const month = cell.text;
				const defaultValue = this.defaultValue
					? Array.isArray(this.defaultValue)
						? this.defaultValue
						: [this.defaultValue]
					: [];
				style.disabled =
					typeof this.disabledDate === "function"
						? datesInMonth(year, month).every(this.disabledDate)
						: false;
				style.current =
					_.findIndex(
						_.$coerceTruthyValueToArray(this.value),
						date => date.getFullYear() === year && date.getMonth() === month
					) >= 0;
				style.today = today.getFullYear() === year && today.getMonth() === month;
				style.default = defaultValue.some(date => this.cellMatchesDate(cell, date));

				if (cell.inRange) {
					style["in-range"] = true;

					if (cell.start) {
						style["start-date"] = true;
					}

					if (cell.end) {
						style["end-date"] = true;
					}
				}
				return style;
			},
			getMonthOfCell(month) {
				const year = this.date.getFullYear();
				return new Date(year, month, 1);
			},
			markRange(minDate, maxDate) {
				minDate = getMonthTimestamp(minDate);
				maxDate = getMonthTimestamp(maxDate) || minDate;
				[minDate, maxDate] = [Math.min(minDate, maxDate), Math.max(minDate, maxDate)];
				const rows = this.rows;
				for (let i = 0, k = rows.length; i < k; i++) {
					const row = rows[i];
					for (let j = 0, l = row.length; j < l; j++) {
						const cell = row[j];
						const index = i * 4 + j;
						const time = new Date(this.date.getFullYear(), index).getTime();

						cell.inRange = minDate && time >= minDate && time <= maxDate;
						cell.start = minDate && time === minDate;
						cell.end = maxDate && time === maxDate;
					}
				}
			},
			handleMouseMove(event) {
				if (!this.rangeState.selecting) return;

				let target = event.target;
				if (target.tagName === "A") {
					target = target.parentNode.parentNode;
				}
				if (target.tagName === "DIV") {
					target = target.parentNode;
				}
				if (target.tagName !== "TD") return;

				const row = target.parentNode.rowIndex;
				const column = target.cellIndex;
				// can not select disabled date
				if (this.rows[row][column].disabled) return;

				// only update rangeState when mouse moves to a new cell
				// this avoids frequent Date object creation and improves performance
				if (row !== this.lastRow || column !== this.lastColumn) {
					this.lastRow = row;
					this.lastColumn = column;
					this.$emit("changerange", {
						minDate: this.minDate,
						maxDate: this.maxDate,
						rangeState: {
							selecting: true,
							endDate: this.getMonthOfCell(row * 4 + column)
						}
					});
				}
			},
			handleMonthTableClick(event) {
				let target = event.target;
				if (target.tagName === "A") {
					target = target.parentNode.parentNode;
				}
				if (target.tagName === "DIV") {
					target = target.parentNode;
				}
				if (target.tagName !== "TD") return;
				if (hasClass(target, "disabled")) return;
				const column = target.cellIndex;
				const row = target.parentNode.rowIndex;
				const month = row * 4 + column;
				const newDate = this.getMonthOfCell(month);
				if (this.selectionMode === "range") {
					if (!this.rangeState.selecting) {
						this.$emit("pick", { minDate: newDate, maxDate: null });
						this.rangeState.selecting = true;
					} else {
						if (newDate >= this.minDate) {
							this.$emit("pick", { minDate: this.minDate, maxDate: newDate });
						} else {
							this.$emit("pick", { minDate: newDate, maxDate: this.minDate });
						}
						this.rangeState.selecting = false;
					}
				} else if (this.selectionMode === "months") {
					const value = this.value || [];
					const year = this.date.getFullYear();
					const newValue =
						_.findIndex(
							value,
							date => date.getFullYear() === year && date.getMonth() === month
						) >= 0
							? removeFromArray(value, date => date.getTime() === newDate.getTime())
							: [...value, newDate];
					this.$emit("pick", newValue);
				} else {
					this.$emit("pick", month);
				}
			}
		},

		computed: {
			rows() {
				// TODO: refactory rows / getCellClasses
				const rows = this.tableRows;
				const disabledDate = this.disabledDate;
				const selectedDate = [];
				const now = getMonthTimestamp(new Date());

				for (let i = 0; i < 3; i++) {
					const row = rows[i];
					for (let j = 0; j < 4; j++) {
						let cell = row[j];
						if (!cell) {
							cell = {
								row: i,
								column: j,
								type: "normal",
								inRange: false,
								start: false,
								end: false
							};
						}

						cell.type = "normal";

						const index = i * 4 + j;
						const time = new Date(this.date.getFullYear(), index).getTime();
						cell.inRange =
							time >= getMonthTimestamp(this.minDate) &&
							time <= getMonthTimestamp(this.maxDate);
						cell.start = this.minDate && time === getMonthTimestamp(this.minDate);
						cell.end = this.maxDate && time === getMonthTimestamp(this.maxDate);
						const isToday = time === now;

						if (isToday) {
							cell.type = "today";
						}
						cell.text = index;
						let cellDate = new Date(time);
						cell.disabled =
							typeof disabledDate === "function" && disabledDate(cellDate);
						cell.selected = _.find(
							selectedDate,
							date => date.getTime() === cellDate.getTime()
						);

						this.$set(row, j, cell);
					}
				}
				return rows;
			}
		}
	});
}
</script>
